/** * Copyright (C) 2008 Mycila (mathieu.carbou@gmail.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.mycila.maven.plugin.license; import com.mycila.maven.plugin.license.document.Document; import com.mycila.maven.plugin.license.document.DocumentFactory; import com.mycila.maven.plugin.license.document.DocumentPropertiesLoader; import com.mycila.maven.plugin.license.document.DocumentType; import com.mycila.maven.plugin.license.header.AdditionalHeaderDefinition; import com.mycila.maven.plugin.license.header.Header; import com.mycila.maven.plugin.license.header.HeaderDefinition; import com.mycila.maven.plugin.license.header.HeaderSource; import com.mycila.maven.plugin.license.header.HeaderType; import com.mycila.maven.plugin.license.util.Selection; import com.mycila.maven.plugin.license.util.resource.ResourceFinder; import com.mycila.xmltool.XMLDoc; import org.apache.maven.artifact.DependencyResolutionRequiredException; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.project.MavenProject; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; import org.apache.maven.settings.crypto.DefaultSettingsDecryptionRequest; import org.apache.maven.settings.crypto.SettingsDecrypter; import org.apache.maven.settings.crypto.SettingsDecryptionRequest; import org.apache.maven.settings.crypto.SettingsDecryptionResult; import org.xml.sax.InputSource; import java.io.File; import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.ServiceLoader; import java.util.concurrent.CompletionService; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import static com.mycila.maven.plugin.license.document.DocumentType.defaultMapping; import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Arrays.deepToString; /** * <b>Date:</b> 18-Feb-2008<br> <b>Author:</b> Mathieu Carbou * (mathieu.carbou@gmail.com) */ public abstract class AbstractLicenseMojo extends AbstractMojo { /** * The base directory, in which to search for project files. */ @Parameter(property = "license.basedir", defaultValue = "${basedir}", required = true) public File basedir; /** * Location of the header. It can be a relative path, absolute path, * classpath resource, any URL. The plugin first check if the name specified * is a relative file, then an absolute file, then in the classpath. If not * found, it tries to construct a URL from the location. */ @Parameter(property = "license.header") public String header; /** * Header, as text, directly in pom file */ @Parameter(property = "license.inlineHeader") public String inlineHeader; /** * Specifies additional header files to use when checking for the presence * of a valid header in your sources. * <br> * When using format goal, this property will be used to detect all valid * headers that don't need formatting. * <br> * When using remove goal, this property will be used to detect all valid * headers that also must be removed. */ @Parameter public String[] validHeaders = new String[0]; /** * Allows the use of external header definitions files. These files are * properties like files. */ @Parameter public String[] headerDefinitions = new String[0]; /** * HeadSections define special regions of a header that allow for dynamic * substitution and validation */ @Parameter public HeaderSection[] headerSections = new HeaderSection[0]; /** * You can set here some properties that you want to use when reading the * header file. You can use in your header file some properties like * ${year}, ${owner} or whatever you want for the name. They will be * replaced when the header file is read by those you specified in the * command line, in the POM and in system environment. */ @Parameter public Map<String, String> properties = new HashMap<String, String>(); /** * Specifies files, which are included in the check. By default, all files * are included. */ @Parameter public String[] includes = new String[0]; /** * Specifies files, which are excluded in the check. By default, only the * files matching the default exclude patterns are excluded. */ @Parameter public String[] excludes = new String[0]; /** * Specify the list of keywords to use to detect a header. A header must * include all keywords to be valid. By default, the word 'copyright' is * used. Detection is done case insensitive. */ @Parameter public String[] keywords = new String[]{"copyright"}; /** * Specify if you want to use default exclusions besides the files you have * excluded. Default exclusions exclude CVS and SVN folders, IDE descriptors * and so on. */ @Parameter(property = "license.useDefaultExcludes", defaultValue = "true") public boolean useDefaultExcludes = true; /** * You can set this flag to true if you want to check the headers for all * modules of your project. Only used for multi-modules projects, to check * for example the header licenses from the parent module for all sub * modules. */ @Parameter(property = "license.aggregate", defaultValue = "false") public boolean aggregate = false; /** * Set mapping between document mapping and a supported type to use. This * section is very useful when you want to customize the supported * extensions. If your project is using file extensions not supported by * default by this plugin, you can add a mapping to attach the extension to * an existing type of comment. The tag name is the new extension name to * support, and the value is the name of the comment type to use. */ @Parameter public LinkedHashMap<String, String> mapping = new LinkedHashMap<String, String>(); /** * Whether to use the default mapping between file extensions and comment * types, or only the one your provide. */ @Parameter(property = "license.useDefaultMapping", defaultValue = "true") public boolean useDefaultMapping = true; /** * Maven license plugin uses concurrency to check license headers. This * factor is used to control the number of threads used to check. The rule * is: * <br> {@code <nThreads> = <number of cores> * concurrencyFactor} * <br> * The default is 1.5. */ @Parameter(property = "license.concurrencyFactor", defaultValue = "1.5") public float concurrencyFactor = 1.5f; /** * Whether to skip the plugin execution */ @Parameter(property = "license.skip", defaultValue = "false") public boolean skip = false; /** * If you do not want to see the list of file having a missing header, you * can add the quiet flag that will shorten the output */ @Parameter(property = "license.quiet", defaultValue = "false") public boolean quiet = false; /** * Set to true if you need a strict check against the headers. By default, * the existence of a header is verified by taking the top portion of a file * and checking if it contains the headers text, not considering special * characters (spaces, tabs, ...). * <br> * We highly recommend to keep this option set to {@code true}. */ @Parameter(property = "license.strictCheck", defaultValue = "true") public boolean strictCheck = true; /** * Specify the encoding of your files. Default to the project source * encoding property (project.build.sourceEncoding). */ @Parameter(property = "license.encoding", defaultValue = "${project.build.sourceEncoding}") public String encoding = "UTF-8"; /** * You can set this flag to false if you do not want the build to fail when * some headers are missing. */ @Parameter(property = "license.failIfMissing", defaultValue = "true") public boolean failIfMissing = true; /** * You can leave this flag on {@code false} if you do not want the build to * fail for files that do not have an implicit or explicit comment style * definition. Setting this explicitly to {@code true} is a safe way to make * sure that the effective file type mappings cover all files included from * your project. * <p> * Default is {@code false} for backwards compatibility reasons. * * @since 2.8 */ @Parameter(property = "license.failIfUnknown", defaultValue = "false") public boolean failIfUnknown = false; /** * If dryRun is enabled, calls to license:format and license:remove will not * overwrite the existing file but instead write the result to a new file * with the same name but ending with `.licensed`. */ @Parameter(property = "license.dryRun", defaultValue = "false") public boolean dryRun = false; /** * Skip the formatting of files which already contain a detected header. */ @Parameter(property = "license.skipExistingHeaders", defaultValue = "false") public boolean skipExistingHeaders = false; @Component public MavenProject project; /** * Maven settings. */ @Component private Settings settings; /** * The decrypter for passwords. */ @Component private SettingsDecrypter settingsDecrypter; private ResourceFinder finder; protected abstract class AbstractCallback implements Callback { /** * Related to {@link #failIfUnknown}. */ private final Collection<File> unknownFiles = new ConcurrentLinkedQueue<File>(); @Override public void onUnknownFile(Document document, Header header) { warn("Unknown file extension: %s", document.getFilePath()); unknownFiles.add(document.getFile()); } public void checkUnknown() throws MojoExecutionException { if (!unknownFiles.isEmpty()) { String msg = "Unable to find a comment style definition for some " + "files. You may want to add a custom mapping for the relevant file extensions."; if (failIfUnknown) { throw new MojoExecutionException(msg); } getLog().warn(msg); } } } @SuppressWarnings({"unchecked"}) public final void execute(final Callback callback) throws MojoExecutionException, MojoFailureException { if (!skip) { if (header == null && (this.inlineHeader == null || this.inlineHeader.isEmpty())) { warn("No header file specified to check for license"); return; } if (!strictCheck) { warn("Property 'strictCheck' is not enabled. Please consider adding <strictCheck>true</strictCheck> in your pom.xml file."); warn("See http://mycila.github.io/license-maven-plugin for more information."); } finder = new ResourceFinder(basedir); try { finder.setCompileClassPath(project.getCompileClasspathElements()); } catch (DependencyResolutionRequiredException e) { throw new MojoExecutionException(e.getMessage(), e); } finder.setPluginClassPath(getClass().getClassLoader()); final HeaderSource headerSource = HeaderSource.of(this.inlineHeader, this.header, this.encoding, this.finder); final Header h = new Header(headerSource, headerSections); debug("Header: %s", h.getLocation()); if (this.validHeaders == null) { this.validHeaders = new String[0]; } final List<Header> validHeaders = new ArrayList<Header>(this.validHeaders.length); for (String validHeader : this.validHeaders) { final HeaderSource validHeaderSource = HeaderSource.of(null, validHeader, this.encoding, this.finder); validHeaders.add(new Header(validHeaderSource, headerSections)); } final List<PropertiesProvider> propertiesProviders = new LinkedList<PropertiesProvider>(); for (PropertiesProvider provider : ServiceLoader.load(PropertiesProvider.class, Thread.currentThread().getContextClassLoader())) { propertiesProviders.add(provider); } final DocumentPropertiesLoader propertiesLoader = new DocumentPropertiesLoader() { @Override public Properties load(Document document) { Properties props = new Properties(); for (Map.Entry<String, String> entry : mergeProperties(document).entrySet()) { if (entry.getValue() != null) { props.setProperty(entry.getKey(), entry.getValue()); } else { props.remove(entry.getKey()); } } for (PropertiesProvider provider : propertiesProviders) { try { final Map<String, String> providerProperties = provider.getAdditionalProperties(AbstractLicenseMojo.this, props, document); if (getLog().isDebugEnabled()) { getLog().debug("provider: " + provider.getClass() + " brought new properties\n" + providerProperties); } for (Map.Entry<String, String> entry : providerProperties.entrySet()) { if (entry.getValue() != null) { props.setProperty(entry.getKey(), entry.getValue()); } else { props.remove(entry.getKey()); } } } catch (Exception e) { getLog().warn("failure occured while calling " + provider.getClass(), e); } } return props; } }; final DocumentFactory documentFactory = new DocumentFactory(basedir, buildMapping(), buildHeaderDefinitions(), encoding, keywords, propertiesLoader); int nThreads = (int) (Runtime.getRuntime().availableProcessors() * concurrencyFactor); ExecutorService executorService = Executors.newFixedThreadPool(nThreads); CompletionService completionService = new ExecutorCompletionService(executorService); int count = 0; debug("Number of execution threads: %s", nThreads); try { for (final String file : listSelectedFiles()) { completionService.submit(new Runnable() { @Override public void run() { Document document = documentFactory.createDocuments(file); debug("Selected file: %s [header style: %s]", document.getFilePath(), document.getHeaderDefinition()); if (document.isNotSupported()) { callback.onUnknownFile(document, h); } else if (document.is(h)) { debug("Skipping header file: %s", document.getFilePath()); } else if (document.hasHeader(h, strictCheck)) { callback.onExistingHeader(document, h); } else { boolean headerFound = false; for (Header validHeader : validHeaders) { if (headerFound = document.hasHeader(validHeader, strictCheck)) { callback.onExistingHeader(document, h); break; } } if (!headerFound) { callback.onHeaderNotFound(document, h); } } } }, null); count++; } while (count-- > 0) { try { completionService.take().get(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { Throwable cause = e.getCause(); if (cause instanceof Error) { throw (Error) cause; } if (cause instanceof MojoExecutionException) { throw (MojoExecutionException) cause; } if (cause instanceof MojoFailureException) { throw (MojoFailureException) cause; } if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } throw new RuntimeException(cause.getMessage(), cause); } } } finally { executorService.shutdownNow(); } } } private Map<String, String> mergeProperties(Document document) { // first put systen environment Map<String, String> props = new LinkedHashMap<String, String>(System.getenv()); // then add ${project.XYZ} properties props.put("project.groupId", project.getGroupId()); props.put("project.artifactId", project.getArtifactId()); props.put("project.version", project.getVersion()); props.put("project.name", project.getName()); props.put("project.description", project.getDescription()); props.put("project.inceptionYear", project.getInceptionYear()); props.put("project.url", project.getUrl()); // then add per document properties props.put("file.name", document.getFile().getName()); // we override by properties in the POM if (this.properties != null) { props.putAll(this.properties); } // then we override by java system properties (command-line -D...) for (String key : System.getProperties().stringPropertyNames()) { props.put(key, System.getProperty(key)); } return props; } private String[] listSelectedFiles() { Selection selection = new Selection(basedir, includes, buildExcludes(), useDefaultExcludes); debug("From: %s", basedir); debug("Including: %s", deepToString(selection.getIncluded())); debug("Excluding: %s", deepToString(selection.getExcluded())); return selection.getSelectedFiles(); } private String[] buildExcludes() { List<String> ex = new ArrayList<String>(); ex.addAll(asList(this.excludes)); if (project != null && project.getModules() != null && !aggregate) { for (String module : (List<String>) project.getModules()) { ex.add(module + "/**"); } } return ex.toArray(new String[ex.size()]); } public final void info(String format, Object... params) { if (!quiet) { getLog().info(format(format, params)); } } public final void debug(String format, Object... params) { if (!quiet) { getLog().debug(format(format, params)); } } public final void warn(String format, Object... params) { if (!quiet) { getLog().warn(format(format, params)); } } private Map<String, String> buildMapping() { Map<String, String> extensionMapping = new LinkedHashMap<String, String>(); // force inclusion of unknow item to manage unknown files extensionMapping.put(DocumentType.UNKNOWN.getExtension(), DocumentType.UNKNOWN.getDefaultHeaderTypeName()); for (Map.Entry<String, String> entry : mapping.entrySet()) { extensionMapping.put(entry.getKey().toLowerCase(), entry.getValue().toLowerCase()); } if (useDefaultMapping) { for (Map.Entry<String, String> entry : defaultMapping().entrySet()) { if (!extensionMapping.containsKey(entry.getKey())) { extensionMapping.put(entry.getKey(), entry.getValue()); } } } return extensionMapping; } private Map<String, HeaderDefinition> buildHeaderDefinitions() throws MojoFailureException { // like mappings, first get default definitions final Map<String, HeaderDefinition> headers = new HashMap<String, HeaderDefinition>(HeaderType.defaultDefinitions()); // and then override them with those provided in properties file for (String resource : headerDefinitions) { try { InputSource source = new InputSource(finder.findResource(resource).openStream()); source.setEncoding(encoding); final AdditionalHeaderDefinition fileDefinitions = new AdditionalHeaderDefinition(XMLDoc.from(source, true)); final Map<String, HeaderDefinition> map = fileDefinitions.getDefinitions(); debug("%d header definitions loaded from '%s'", map.size(), resource); headers.putAll(map); } catch (IOException ex) { throw new MojoFailureException("Error reading header definition: " + resource, ex); } } // force inclusion of unknow item to manage unknown files headers.put(HeaderType.UNKNOWN.getDefinition().getType(), HeaderType.UNKNOWN.getDefinition()); return headers; } /** * Returns the list of servers with decrypted passwords. * * @return list of servers with decrypted passwords. */ List<Server> getDecryptedServers() { final SettingsDecryptionRequest settingsDecryptionRequest = new DefaultSettingsDecryptionRequest(); settingsDecryptionRequest.setServers(settings.getServers()); final SettingsDecryptionResult decrypt = settingsDecrypter.decrypt(settingsDecryptionRequest); return decrypt.getServers(); } /** * Retrieves the credentials for the given server or null if none could be * found * * @param serverID * @return */ public Credentials findCredentials(String serverID) { List<Server> decryptedServers = getDecryptedServers(); for (Server ds : decryptedServers) { if (ds.getId().equals(serverID)) { getLog().debug("credentials have been found for server: " + serverID + ", login:" + ds.getUsername() + ", password:" + starEncrypt(ds.getPassword())); return new Credentials(ds.getUsername(), ds.getPassword()); } } getLog().debug("no credentials found for server: " + serverID); return null; } static String starEncrypt(String str) { if (str == null) { return null; } return str.replaceAll(".", "*"); } }